/*
 * Copyright 2009, Qys Creations 
 * 
 *  This file is part of Chameleon DB.
 *
 *  Chameleon DB is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Chameleon DB is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Chameleon DB.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * */

import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.util.*;

public class db_book {
	private ArrayList<ArrayList<db_page>> oGroups = new ArrayList<ArrayList<db_page>>();
	private ArrayList<db_wall_lock> oGroupLocks = new ArrayList<db_wall_lock>();

	private long lNextPagePointer = 0;
	private long lCurrentPagePointer = 0;
	private db_bytes oPadding = null;
	private db_file oFile = null;
	private db_wall_lock oFileLock = new db_wall_lock();
	private FileChannel oChannel = null;
	
	// Transaction vars
	private long lTransactionId = 0;
	private Object oTransactionLock = new Object();
	db_book(RandomAccessFile oFilePointer){
		// initialize
		oChannel = oFilePointer.getChannel();
		oFile = new db_file(oChannel);
		try {
			if (oFile.length() > 0 ) {
				oFile.seek(0);
				long lReadNextPartionPage =  0;
				while (true) {
					lReadNextPartionPage = oFile.readLong();
					long lReadTo = oFile.readLong();
					for (int i = 0; i < (lReadTo / 16)-1; i++) {
						long lGroupVal = oFile.readLong();
						long lFP = oFile.readLong();
						if (lGroupVal >= oGroups.size()) {
							if (lGroupVal >= 5)
							System.out.println("FAIL FAIL FAIL, unreasonable group number read " + lGroupVal);
							else
							for (int m = oGroups.size()-1; m < lGroupVal; m++)
								createGroup();
						}
						oGroups.get((int)lGroupVal).add(new db_page(lGroupVal, lFP));
					}
					lNextPagePointer = lReadTo;

					if (lReadNextPartionPage == 0)
						break;
					lCurrentPagePointer = lReadNextPartionPage;
					oFile.seek(lReadNextPartionPage);
				}
				// Check pages
				for (int m = 0; m < oGroups.size(); m++) {
					System.out.println("Group " + m + " has " + oGroups.get(m).size() + " pages");
				}
			}
		} catch (Exception e) {e.printStackTrace();}
	}
	
	public db_reading_glass getGlass() {
		return new db_reading_glass(this, oChannel);
	}
	
	public int groupCount() {
		return oGroups.size();
	}
	
	public byte[] read(long lGroup, long lPosition, int iLength, FileChannel oFilePointer) {
		int lRead = 0;
		int lFrom = 0;
		int lTo = 0;

		byte[] bData = new byte[iLength];;

		//System.out.println(Thread.currentThread().getId() + " read pos " + iLength);
		int iPageFrom = (int) Math.floor(lPosition / (db_util.DB_PAGE_SIZE-16));
		int iPageTo = (int) Math.floor(( lPosition + iLength) / (db_util.DB_PAGE_SIZE-16));

	
		lFrom = (int) (16 + (lPosition - iPageFrom * (db_util.DB_PAGE_SIZE-16)));
		lTo = iLength;

		//System.out.println( Thread.currentThread().getId() +  " Page reading  range is: " + iPageFrom + "-" + iPageTo + " " + lGroup + " " + oGroups.get((int)lGroup).size());
		for (int i = iPageFrom; i <= iPageTo; i++) {
			//if (lTo >  (db_util.DB_PAGE_SIZE-16))
			//	lTo = (db_util.DB_PAGE_SIZE-16) - lFrom;
			if (lTo + lFrom >  (db_util.DB_PAGE_SIZE))
				lTo = (db_util.DB_PAGE_SIZE) - lFrom;
			//System.out.println("Reading from " + lFrom + " " + lRead + " " + lTo + " " + iLength );
			//System.out.println("Read so far: " + lRead + " group length: " + length(lGroup, oFilePointer));

			System.arraycopy(oGroups.get((int)lGroup).get(i).read(oFilePointer), lFrom, bData, lRead, lTo);
			
			lRead += lTo;

			lFrom = 16;
			lTo = iLength - lRead;
		}
		return bData;
	}
	
	public int createGroup() {
		int iVal = 0;
		try {
			oFileLock.getWallLock();
			
			oGroups.add(new ArrayList<db_page>());
			oGroupLocks.add(new db_wall_lock());
			iVal = oGroups.size() -1;
			//System.out.println("Group " + iVal + " created");
			oFileLock.releaseWallLock();
			
		} catch (Exception e) {e.printStackTrace();}
		return iVal;
	}
	
	public void write(long lGroup, long lPosition, byte[] bData, FileChannel oFilePointer, db_transaction oTrans) {
		
		int lWritten = 0;
		int lFrom = 0;
		int lTo = 0;

		byte[] bWriteBuffer = new byte[db_util.DB_PAGE_SIZE];

		// Okay do we have enough room in the current pages?
		try {
			oGroupLocks.get((int)lGroup).getWallLock();

			double dLength = lPosition + bData.length;
			double dCurrentLength = length(lGroup, oFilePointer);

			int iCurrentPages = (int) Math.floor((dCurrentLength / (db_util.DB_PAGE_SIZE - 16))) +1;

			int iNeededPages = (int) Math.ceil(dLength / (db_util.DB_PAGE_SIZE - 16));

			//System.out.println(Thread.currentThread().getId() + " thinks on basis of " + dCurrentLength + " there is " + iCurrentPages + " pages and needs to be " + iNeededPages);

			if (iCurrentPages < iNeededPages) {
				// hm no, lets add some new pages then
				for (int n = iCurrentPages; n < iNeededPages ; n++) {
					int iBefore = oGroups.get((int)lGroup).size();
					System.out.println("Adding page; pos " + lPosition + " length " + bData.length + " group " + lGroup + " pages " + oGroups.get((int)lGroup).size() + " current length " + dCurrentLength);
					addPage(lGroup, n);
					if (iBefore ==  oGroups.get((int)lGroup).size())
						System.out.println("FAIL FAIL FAIL, page creation was attempted but failed unexpectedly");
				}
			}
			oGroupLocks.get((int)lGroup).releaseWallLock();
		} catch (Exception e) {e.printStackTrace();}


		// All right, lets look up the usable pages

		int iPageFrom = (int) Math.floor(lPosition / (db_util.DB_PAGE_SIZE-16));
		int iPageTo = (int) Math.floor(( lPosition + bData.length) / (db_util.DB_PAGE_SIZE-16));
		lFrom = (int) (16 + (lPosition - iPageFrom * (db_util.DB_PAGE_SIZE-16)));
		lTo = bData.length;
		
		for (int i = iPageFrom; i <= iPageTo; i++) {
			boolean bPageNotFound = true;
			for (int k = 0; k < oTrans.oPages.size(); k++) {
				if (oTrans.oPages.get(k).equals(oGroups.get((int)lGroup).get(i)))
					bPageNotFound = false;
			}
			db_page oPage = oGroups.get((int)lGroup).get(i);

						
			// Add to transaction pages
			if (bPageNotFound) {
				//System.out.println("adding to transaction pages");
				// Mark the page for writing
				oPage.begin();
				oTrans.oPages.add(oPage);
			}
			if (lTo + lFrom > (db_util.DB_PAGE_SIZE))
				lTo = (db_util.DB_PAGE_SIZE) - lFrom;

			
			//System.out.println("Pages " + i + " " + oGroups.get((int)lGroup).size());
			// Get old data
			// Important not to use normal read function, since that would cause a deadlock, and use invalid data.
			System.arraycopy(oPage.bDataWrite, 0, bWriteBuffer, 0,  db_util.DB_PAGE_SIZE);
			//System.out.println("Writing from " + lFrom + " and " + lTo + " bytes forward, lWr "  + lWritten);
			// The actual write to the buffer - much alarm for this...
			System.arraycopy(bData, lWritten, bWriteBuffer, lFrom, lTo);

			// Update page header information
			// With new length of content
			db_bytes oBytes = new db_bytes(bWriteBuffer);
			oBytes.seek(8);
			long lPageSize = oBytes.readLong();

			//System.out.println(lFrom + " " + lTo);
			if (lPageSize < lFrom + lTo)
				lPageSize = lFrom + lTo;

			oBytes.seek(8);
			oBytes.write(lPageSize);

			// Write to page
			oPage.write(oBytes.getBytes());

			// Note progress 
			lWritten += lTo;
			lFrom = 16;
			lTo = bData.length - lWritten;
			
		}
	}
	
	public db_transaction begin() {
		long lNewTransactionId = 0;
		synchronized (oTransactionLock) {
			lNewTransactionId = lTransactionId++;
		}
		return new db_transaction(lNewTransactionId);
	}
	
	public void rollback(db_transaction oTrans) {
		for (int i = 0; i < oTrans.oPages.size(); i++)
			oTrans.oPages.get(i).rollback();
	}
	
	public void commit(db_transaction oTrans, FileChannel oFilePointer) {
		for (int i = 0; i < oTrans.oPages.size(); i++)
			oTrans.oPages.get(i).commit();
		for (int i = 0; i < oTrans.oPages.size(); i++)
			oTrans.oPages.get(i).save(oFilePointer);
		for (int i = 0; i < oTrans.oPages.size(); i++)
			oTrans.oPages.get(i).finish();
		//System.out.println(oTrans.oPages.size() + " pages written to disk");
	}
	
	public void append(long lGroup, byte[] bData, FileChannel oFilePointer) {
		int lWritten = 0;
		int lFrom = 0;
		int lTo = 0;

		byte[] bWriteBuffer = new byte[db_util.DB_PAGE_SIZE];

		// Okay do we have enough room in the current pages?
		double dCurrentLength = 0;
		try {
			oGroupLocks.get((int)lGroup).getWallLock();

			dCurrentLength = length(lGroup, oFilePointer);
			double dLength = dCurrentLength + bData.length;
			

			int iCurrentPages = (int) Math.floor((dCurrentLength / (db_util.DB_PAGE_SIZE - 16))) +1;

			int iNeededPages = (int) Math.ceil(dLength / (db_util.DB_PAGE_SIZE - 16));

			//System.out.println(Thread.currentThread().getId() + " thinks on basis of " + dCurrentLength + " there is " + iCurrentPages + " pages and needs to be " + iNeededPages);

			if (iCurrentPages < iNeededPages) {
				// hm no, lets add some new pages then
				for (int n = iCurrentPages; n < iNeededPages ; n++) {
					int iBefore = oGroups.get((int)lGroup).size();
					//System.out.println("Adding page; pos " + dCurrentLength + " length " + bData.length + " group " + lGroup + " pages " + oGroups.get((int)lGroup).size() + " current length " + dCurrentLength);
					addPage(lGroup, n);
					if (iBefore ==  oGroups.get((int)lGroup).size())
						System.out.println("FAIL FAIL FAIL, page creation was attempted but failed unexpectedly");
				}
			}
			oGroupLocks.get((int)lGroup).releaseWallLock();
		} catch (Exception e) {e.printStackTrace();}


		// All right, lets look up the usable pages

		int iPageFrom = (int) Math.floor(dCurrentLength / (db_util.DB_PAGE_SIZE-16));
		int iPageTo = (int) Math.floor((dCurrentLength + bData.length) / (db_util.DB_PAGE_SIZE-16));
		lFrom = (int) (16 + (dCurrentLength - iPageFrom * (db_util.DB_PAGE_SIZE-16)));
		lTo = bData.length;
		//System.out.println("Page writting range is: " + iPageFrom + "-" + iPageTo);
		for (int i = iPageFrom; i <= iPageTo; i++) {

			if (lTo + lFrom > (db_util.DB_PAGE_SIZE))
				lTo = (db_util.DB_PAGE_SIZE) - lFrom;

			// Get old data
			//System.out.println("Pages " + i + " " + oGroups.get((int)lGroup).size());
			System.arraycopy(oGroups.get((int)lGroup).get(i).read(oFilePointer), 0, bWriteBuffer, 0,  db_util.DB_PAGE_SIZE);
			//System.out.println("Writing from " + lFrom + " and " + lTo + " bytes forward, lWr "  + lWritten);
			System.arraycopy(bData, lWritten, bWriteBuffer, lFrom, lTo);

			// Update page header information
			db_bytes oBytes = new db_bytes(bWriteBuffer);
			oBytes.seek(8);
			long lPageSize = oBytes.readLong();
			//System.out.println(lFrom + " " + lTo);
			if (lPageSize < lFrom + lTo)
				lPageSize = lFrom + lTo;

			oBytes.seek(8);
			oBytes.write(lPageSize);

			// Write to page
			oGroups.get((int)lGroup).get(i).write(oBytes.getBytes());

			// Note progress 
			lWritten += lTo;
			lFrom = 16;
			lTo = bData.length - lWritten;
		}
	}
	
	public long length(long lGroup, FileChannel oFilePointer) {
		long lLength = 0;
		if (oGroups.get((int)lGroup).size() == 0)
			return -1;
		lLength = (oGroups.get((int)lGroup).size()-1) * (db_util.DB_PAGE_SIZE-16);
		db_bytes oBytes = new db_bytes(oGroups.get((int)lGroup).get(oGroups.get((int)lGroup).size()-1).read(oFilePointer) );
		oBytes.seek(8);
		long lExtra = oBytes.readLong()-16;
		if (lExtra == -16)
			System.out.println(Thread.currentThread().getId() + " FAIL FAIL FAIL, length of page was not written to disk, now being read incorrectly");
		lLength += lExtra;
		
		return lLength;
	}

	public static void newBook(db_file oFilePointer) {
		try {
			db_bytes oBytes = new db_bytes(db_util.DB_PAGE_SIZE);

			oBytes.write((long)0);
			oBytes.write((long)16);
			oFilePointer.seek(0);
			oFilePointer.write(oBytes.getBytes());

		} catch (Exception e) {e.printStackTrace();}
	}

	
	
	
	public void addPage(long lGroup, int iPageNumber) {
		try {
			oFileLock.getWallLock();
			
			
			//System.out.println("Size: " + iPageNumber + " " + oGroups.size());
			//if (iPageNumber > oGroups.size()  ) {
			//	oFileLock.releaseWallLock();
			//	return;
			//}
			//System.out.println(Thread.currentThread().getId() + " adding page");
			// Create new page
			long lNewPagePos = 16;
			oPadding = new db_bytes(db_util.DB_PAGE_SIZE);
			oPadding.write(lGroup);
			oPadding.write(lNewPagePos);
			
			oPadding.seek(8);
			

			
			// Append to end of file
			long lNewPagePosition = oFile.length();
			oFile.seek(lNewPagePosition);
			oFile.write(oPadding.getBytes());
			
			//System.out.println(Thread.currentThread().getId() + " self check " + lNewPagePosition + " and we have added " + oPadding.getBytes().length);
			//System.out.println(Thread.currentThread().getId() + " file length " +  oFile.length());
			
			oFile.seek(lNewPagePosition);
			
			long lVal = oFile.readLong();
			if (lVal != lGroup)
				System.out.println(Thread.currentThread().getId() + " XXX FAIL FAIL FAIL, page group is rotten " + lVal + " != " + lGroup);
			lVal = oFile.readLong();
			if (lVal != 16)
				System.out.println(Thread.currentThread().getId() + " XXX FAIL FAIL FAIL, page length is rotten " + lVal);
			// Add new page position to partition page
			if (lNextPagePointer + 16 > db_util.DB_PAGE_SIZE) {
				// Gotta make a new page definition page
				oFile.seek(lCurrentPagePointer);
				long lEndOfFile = oFile.length();
				oFile.writeLong(lEndOfFile);
				db_bytes oBytes = new db_bytes(db_util.DB_PAGE_SIZE);
				oBytes.write((long)0);
				oBytes.write((long)16);
				oFile.seek(lEndOfFile);
				oFile.write(oBytes.getBytes());
				lCurrentPagePointer = lEndOfFile;
				lNextPagePointer = 16;
			}
			oFile.seek(lCurrentPagePointer + lNextPagePointer);
			oFile.writeLong(lGroup);
			oFile.writeLong(lNewPagePosition);
			// Update partition page information
			oFile.seek(lCurrentPagePointer + 8);
			lNextPagePointer += 16;
			oFile.writeLong(lNextPagePointer);
			
			
			oGroups.get((int)lGroup).add(new db_page(lGroup, lNewPagePosition));

			oFileLock.releaseWallLock();
		} catch (Exception e) {e.printStackTrace();}
	}

}
